package loader

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"syscall"
	"time"
	"unsafe"

	"golang.org/x/sys/unix"
)

// FanotifyMonitor monitors filesystem events using fanotify
// This is the core component for on-demand loading trigger
type FanotifyMonitor struct {
	fd       int
	watchMap map[string]bool
	mu       sync.RWMutex
	events   chan FileEvent
	stop     chan struct{}
	wg       sync.WaitGroup
}

// FileEvent represents a filesystem event
type FileEvent struct {
	Path      string
	EventType EventType
	PID       int
	Timestamp int64
}

// EventType represents the type of filesystem event
type EventType int

const (
	FileAccess EventType = iota
	FileOpen
	FileRead
	FileWrite
	FileClose
)

// NewFanotifyMonitor creates a new fanotify monitor
func NewFanotifyMonitor() (*FanotifyMonitor, error) {
	// Create fanotify file descriptor
	fd, err := unix.FanotifyInit(unix.FAN_CLASS_CONTENT, unix.O_RDONLY|unix.O_LARGEFILE)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize fanotify: %w", err)
	}

	monitor := &FanotifyMonitor{
		fd:       fd,
		watchMap: make(map[string]bool),
		events:   make(chan FileEvent, 1000),
		stop:     make(chan struct{}),
	}

	return monitor, nil
}

// AddWatch adds a path to monitor
func (m *FanotifyMonitor) AddWatch(path string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if _, exists := m.watchMap[path]; exists {
		return nil // Already watching
	}

	// Resolve absolute path
	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("failed to resolve path: %w", err)
	}

	// Add fanotify mark for the path
	err = unix.FanotifyMark(m.fd, unix.FAN_MARK_ADD,
		unix.FAN_OPEN|unix.FAN_ACCESS|unix.FAN_CLOSE|unix.FAN_EVENT_ON_CHILD,
		unix.AT_FDCWD, absPath)
	if err != nil {
		return fmt.Errorf("failed to add fanotify mark: %w", err)
	}

	m.watchMap[absPath] = true
	return nil
}

// RemoveWatch removes a path from monitoring
func (m *FanotifyMonitor) RemoveWatch(path string) error {
	m.mu.Lock()
	defer m.mu.Unlock()

	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("failed to resolve path: %w", err)
	}

	if _, exists := m.watchMap[absPath]; !exists {
		return nil // Not watching
	}

	// Remove fanotify mark
	err = unix.FanotifyMark(m.fd, unix.FAN_MARK_REMOVE,
		unix.FAN_OPEN|unix.FAN_ACCESS|unix.FAN_CLOSE|unix.FAN_EVENT_ON_CHILD,
		unix.AT_FDCWD, absPath)
	if err != nil {
		return fmt.Errorf("failed to remove fanotify mark: %w", err)
	}

	delete(m.watchMap, absPath)
	return nil
}

// Start starts the event monitoring loop
func (m *FanotifyMonitor) Start(ctx context.Context) error {
	m.wg.Add(1)
	go m.eventLoop(ctx)
	return nil
}

// Stop stops the event monitoring
func (m *FanotifyMonitor) Stop() error {
	close(m.stop)
	m.wg.Wait()

	if m.fd >= 0 {
		unix.Close(m.fd)
		m.fd = -1
	}

	return nil
}

// Events returns the event channel
func (m *FanotifyMonitor) Events() <-chan FileEvent {
	return m.events
}

// GetWatchedPaths returns all watched paths
func (m *FanotifyMonitor) GetWatchedPaths() []string {
	m.mu.RLock()
	defer m.mu.RUnlock()

	paths := make([]string, 0, len(m.watchMap))
	for path := range m.watchMap {
		paths = append(paths, path)
	}
	return paths
}

// eventLoop is the main event processing loop
func (m *FanotifyMonitor) eventLoop(ctx context.Context) {
	defer m.wg.Done()

	buffer := make([]byte, 4096)

	for {
		select {
		case <-ctx.Done():
			return
		case <-m.stop:
			return
		default:
			n, err := unix.Read(m.fd, buffer)
			if err != nil {
				if err == syscall.EINTR {
					continue
				}
				// Log error but continue
				fmt.Printf("Error reading fanotify events: %v\n", err)
				continue
			}

			if n > 0 {
				m.processEvents(buffer[:n])
			}
		}
	}
}

// processEvents processes raw fanotify events
func (m *FanotifyMonitor) processEvents(buffer []byte) {
	offset := 0

	for offset < len(buffer) {
		if len(buffer)-offset < int(unsafe.Sizeof(unix.FanotifyEventMetadata{})) {
			break
		}

		metadata := (*unix.FanotifyEventMetadata)(unsafe.Pointer(&buffer[offset]))
		if metadata.Metadata_len == 0 {
			break
		}

		// Process the event
		m.processEvent(metadata)

		offset += int(metadata.Metadata_len)
	}
}

// processEvent processes a single fanotify event
func (m *FanotifyMonitor) processEvent(metadata *unix.FanotifyEventMetadata) {
	// Get file path from file descriptor
	path, err := m.getPathFromFD(int(metadata.Fd))
	if err != nil {
		// Log error but continue
		fmt.Printf("Error getting path from fd: %v\n", err)
		return
	}

	// Determine event type
	eventType := m.getEventType(metadata.Mask)

	// Create file event
	event := FileEvent{
		Path:      path,
		EventType: eventType,
		PID:       int(metadata.Pid),
		Timestamp: time.Now().Unix(),
	}

	// Send event to channel (non-blocking)
	select {
	case m.events <- event:
	default:
		// Channel full, drop event
		fmt.Printf("Event channel full, dropping event for %s\n", path)
	}

	// Close the file descriptor
	unix.Close(int(metadata.Fd))
}

// getPathFromFD gets the file path from a file descriptor
func (m *FanotifyMonitor) getPathFromFD(fd int) (string, error) {
	// Read the symbolic link in /proc/self/fd/
	link := fmt.Sprintf("/proc/self/fd/%d", fd)
	path, err := os.Readlink(link)
	if err != nil {
		return "", fmt.Errorf("failed to read link: %w", err)
	}

	return path, nil
}

// getEventType converts fanotify mask to EventType
func (m *FanotifyMonitor) getEventType(mask uint64) EventType {
	if mask&unix.FAN_ACCESS != 0 {
		return FileAccess
	}
	if mask&unix.FAN_OPEN != 0 {
		return FileOpen
	}
	if mask&unix.FAN_CLOSE_WRITE != 0 {
		return FileWrite
	}
	if mask&unix.FAN_CLOSE_NOWRITE != 0 {
		return FileClose
	}
	return FileAccess // Default
}

// IsAvailable checks if fanotify is available on the system
func IsFanotifyAvailable() bool {
	fd, err := unix.FanotifyInit(unix.FAN_CLASS_CONTENT, unix.O_RDONLY)
	if err != nil {
		return false
	}
	unix.Close(fd)
	return true
}